/*
 * Copyright 2012 to 2016 bigbiff/Dees_Troy TeamWin
 * This file is part of TWRP/TeamWin Recovery Project.
 *
 * TWRP is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * TWRP is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with TWRP.  If not, see <http://www.gnu.org/licenses/>.
 */

// input.cpp - GUIInput object

#include "gui/input.hpp"

#include <linux/input.h>

#include "data.hpp"

#include "gui/keyboard.hpp"

#define TW_INPUT_NO_UPDATE -1000 // Magic value for HandleTextLocation when no change in scrolling has occurred

GUIInput::GUIInput(xml_node<>* node)
    : GUIObject(node)
{
    xml_attribute<>* attr;
    xml_node<>* child;

    mInputText = nullptr;
    mAction = nullptr;
    mBackground = nullptr;
    mCursor = nullptr;
    mFont = nullptr;
    mRendered = false;
    HasMask = false;
    DrawCursor = false;
    isLocalChange = true;
    HasAllowed = false;
    HasDisabled = false;
    cursorX = textWidth = scrollingX = mFontHeight = mFontY = lastX = 0;
    mBackgroundX = mBackgroundY = mBackgroundW = mBackgroundH = MinLen = MaxLen = 0;
    mCursorLocation = -1; // -1 is always the end of the string
    CursorWidth = 3;
    ConvertStrToColor("black", &mBackgroundColor);
    ConvertStrToColor("white", &mCursorColor);
    mValue = "";
    displayValue = "";

    if (!node) {
        return;
    }

    // Load text directly from the node
    mInputText = new GUIText(node);
    // Load action directly from the node
    mAction = new GUIAction(node);

    if (mInputText->Render() < 0) {
        delete mInputText;
        mInputText = nullptr;
    }

    // Load the background
    child = FindNode(node, "background");
    if (child) {
        mBackground = LoadAttrImage(child, "resource");
        mBackgroundColor = LoadAttrColor(child, "color", mBackgroundColor);
    }
    if (mBackground && mBackground->GetResource()) {
        mBackgroundW = mBackground->GetWidth();
        mBackgroundH = mBackground->GetHeight();
    }

    // Load the cursor color
    child = FindNode(node, "cursor");
    if (child) {
        mCursor = LoadAttrImage(child, "resource");
        mCursorColor = LoadAttrColor(child, "color", mCursorColor);
        attr = child->first_attribute("hasfocus");
        if (attr) {
            std::string focus = attr->value();
            SetInputFocus(atoi(focus.c_str()));
        }
        CursorWidth = LoadAttrIntScaleX(child, "width", CursorWidth);
    }
    DrawCursor = HasInputFocus;

    // Load the font
    child = FindNode(node, "font");
    if (child) {
        mFont = LoadAttrFont(child, "resource");
        mFontHeight = mFont->GetHeight();
    }

    child = FindNode(node, "data");
    if (child) {
        attr = child->first_attribute("name");
        if (attr) {
            mVariable = attr->value();
        }
        attr = child->first_attribute("default");
        if (attr) {
            DataManager::SetValue(mVariable, attr->value());
        }
        mMask = LoadAttrString(child, "mask");
        HasMask = !mMask.empty();
    }

    // Load input restrictions
    child = FindNode(node, "restrict");
    if (child) {
        MinLen = LoadAttrInt(child, "minlen", MinLen);
        MaxLen = LoadAttrInt(child, "maxlen", MaxLen);
        AllowedList = LoadAttrString(child, "allow");
        HasAllowed = !AllowedList.empty();
        DisabledList = LoadAttrString(child, "disable");
        HasDisabled = !DisabledList.empty();
    }

    // Load the placement
    LoadPlacement(FindNode(node, "placement"), &mRenderX, &mRenderY, &mRenderW, &mRenderH);
    SetActionPos(mRenderX, mRenderY, mRenderW, mRenderH);

    if (mInputText && mFontHeight && mFontHeight < (unsigned) mRenderH) {
        mFontY = ((mRenderH - mFontHeight) / 2) + mRenderY;
        mInputText->SetRenderPos(mRenderX, mFontY);
    } else {
        mFontY = mRenderY;
    }

    if (mInputText) {
        mInputText->SetMaxWidth(0);
    }
}

GUIInput::~GUIInput()
{
    delete mInputText;
    delete mAction;
}

void GUIInput::HandleTextLocation(int x)
{
    mRendered = false;
    if (textWidth <= mRenderW) {
        if (x != TW_INPUT_NO_UPDATE) {
            lastX = x;
        }
        scrollingX = 0;
        return;
    }
    if (scrollingX + textWidth < mRenderW) {
        scrollingX = mRenderW - textWidth;
    }

    if (x == TW_INPUT_NO_UPDATE) {
        return;
    }

    scrollingX += x - lastX;
    if (scrollingX > 0) {
        scrollingX = 0;
    } else if (scrollingX + textWidth < mRenderW) {
        scrollingX = mRenderW - textWidth;
    }
    lastX = x;
}

void GUIInput::UpdateDisplayText()
{
    void* fontResource = nullptr;

    if (mFont) {
        fontResource = mFont->GetResource();
    } else {
        textWidth = 0;
        return;
    }

    DataManager::GetValue(mVariable, mValue);
    if (HasMask) {
        int index, string_size = mValue.size();
        std::string maskedValue;
        for (index=0; index < string_size; index++) {
            maskedValue += mMask;
        }
        displayValue = maskedValue;
    } else {
        displayValue = mValue;
    }

    textWidth = gr_ttf_measureEx(displayValue.c_str(), fontResource);
}

void GUIInput::HandleCursorByTouch(int x)
{
    // Uses x to find mCursorLocation and cursorX
    if (displayValue.size() == 0) {
        mCursorLocation = -1;
        cursorX = mRenderX;
        return;
    }

    void* fontResource = nullptr;
    if (mFont) {
        fontResource = mFont->GetResource();
    } else {
        return;
    }

    std::string cursorString;
    unsigned index = 0, displaySize = displayValue.size();
    int prevX = mRenderX + scrollingX;

    for (index = 0; index <= displaySize; index++) {
        cursorString = displayValue.substr(0, index);
        cursorX = gr_ttf_measureEx(cursorString.c_str(), fontResource) + mRenderX + scrollingX;
        if (cursorX > x) {
            if (index > 0 && x <= cursorX - ((x - prevX) / 2) && prevX >= mRenderX) {
                // This helps make sure that we can place the cursor before the very first char if the first char is
                // is fully visible while also still letting us place the cursor after the last char if fully visible
                mCursorLocation = index - 1;
                cursorX = prevX;
                return;
            }
            mCursorLocation = index;
            if (cursorX > mRenderX + mRenderW) {
                cursorX = prevX; // This makes sure that the cursor doesn't get placed after the end of the input box
                mCursorLocation--;
                return;
            }
            if (cursorX >= mRenderX) {
                return; // This makes sure that the cursor doesn't get placed before the beginning of the input box
            }
        }
        prevX = cursorX;
    }
    mCursorLocation = -1; // x is at or past the end of the string
}

void GUIInput::HandleCursorByText()
{
    // Uses mCursorLocation to find cursorX
    if (!DrawCursor) {
        return;
    }

    void* fontResource = nullptr;
    if (mFont) {
        fontResource = mFont->GetResource();
    } else {
        return;
    }

    int cursorTextWidth = textWidth; // width of text to the left of the cursor

    if (mCursorLocation != -1) {
        std::string cursorDisplay = displayValue;
        cursorDisplay.resize(mCursorLocation);
        cursorTextWidth = gr_ttf_measureEx(cursorDisplay.c_str(), fontResource);
    }
    cursorX = mRenderX + cursorTextWidth + scrollingX;
    if (cursorX >= mRenderX + mRenderW) {
        scrollingX = mRenderW - cursorTextWidth;
        cursorX = mRenderX + mRenderW - CursorWidth;
    } else if (cursorX < mRenderX) {
        scrollingX = cursorTextWidth * -1;
        cursorX = mRenderX;
    }
}

int GUIInput::Render()
{
    if (!isConditionTrue()) {
        mRendered = false;
        return 0;
    }

    void* fontResource = nullptr;
    if (mFont) {
        fontResource = mFont->GetResource();
    }

    // First step, fill background
    gr_color(mBackgroundColor.red, mBackgroundColor.green, mBackgroundColor.blue, 255);
    gr_fill(mRenderX, mRenderY, mRenderW, mRenderH);

    // Next, render the background resource (if it exists)
    if (mBackground && mBackground->GetResource()) {
        mBackgroundX = mRenderX + ((mRenderW - mBackgroundW) / 2);
        mBackgroundY = mRenderY + ((mRenderH - mBackgroundH) / 2);
        gr_blit(mBackground->GetResource(), 0, 0, mBackgroundW, mBackgroundH, mBackgroundX, mBackgroundY);
    }

    int ret = 0;

    // Render the text
    if (mInputText) {
        mInputText->SetRenderPos(mRenderX + scrollingX, mFontY);
        mInputText->SetText(displayValue);
        gr_clip(mRenderX, mRenderY, mRenderW, mRenderH);
        ret = mInputText->Render();
        gr_noclip();
    }
    if (ret < 0) {
        return ret;
    }

    if (HasInputFocus && DrawCursor) {
        // Render the cursor
        gr_color(mCursorColor.red, mCursorColor.green, mCursorColor.blue, 255);
        gr_fill(cursorX, mFontY, CursorWidth, mFontHeight);
    }

    mRendered = true;
    return ret;
}

int GUIInput::Update()
{
    if (!isConditionTrue()) {
        return (mRendered ? 2 : 0);
    }
    if (!mRendered) {
        return 2;
    }

    int ret = 0;

    if (mInputText) {
        ret = mInputText->Update();
    }
    if (ret < 0) {
        return ret;
    }

    return ret;
}

int GUIInput::GetSelection(int x, int y)
{
    if (x < mRenderX || x - mRenderX > mRenderW || y < mRenderY || y - mRenderY > mRenderH) {
        return -1;
    }
    return (x - mRenderX);
}

int GUIInput::NotifyTouch(TOUCH_STATE state, int x, int y)
{
    static int startSelection = -1;
    void* fontResource = nullptr;

    if (mFont) {
        fontResource = mFont->GetResource();
    }

    if (!isConditionTrue()) {
        return -1;
    }

    if (!HasInputFocus) {
        if (state != TOUCH_RELEASE) {
            return 0; // Only change focus if touch releases within the input box
        }
        if (GetSelection(x, y) >= 0) {
            // When changing focus, we don't scroll or change the cursor location
            PageManager::SetKeyBoardFocus(0);
            PageManager::NotifyCharInput(0);
            SetInputFocus(1);
            DrawCursor = true;
            mRendered = false;
        }
    } else {
        switch (state) {
        case TOUCH_HOLD:
        case TOUCH_REPEAT:
            break;
        case TOUCH_START:
            startSelection = GetSelection(x,y);
            lastX = x;
            DrawCursor = false;
            mRendered = false;
            break;

        case TOUCH_DRAG:
            // Check if we dragged out of the selection window
            if (GetSelection(x, y) == -1) {
                lastX = 0;
                break;
            }

            DrawCursor = false;

            // Provide some debounce on initial touches
            if (startSelection != -1 && abs(x - lastX) < 6) {
                break;
            }

            startSelection = -1;
            if (lastX != x) {
                HandleTextLocation(x);
            }
            break;

        case TOUCH_RELEASE:
            // We've moved the cursor location
            mRendered = false;
            DrawCursor = true;
            HandleCursorByTouch(x);
            break;
        }
    }
    return 0;
}

int GUIInput::NotifyVarChange(const std::string& varName, const std::string& value)
{
    GUIObject::NotifyVarChange(varName, value);

    if (varName == mVariable) {
        if (!isLocalChange) {
            UpdateDisplayText();
            HandleTextLocation(TW_INPUT_NO_UPDATE);
        } else {
            isLocalChange = false;
        }
        return 0;
    }
    if (varName.empty()) {
        UpdateDisplayText();
        HandleTextLocation(TW_INPUT_NO_UPDATE);
        HandleCursorByText();
    }
    return 0;
}

int GUIInput::NotifyKey(int key, bool down)
{
    if (!HasInputFocus || !down) {
        return 1;
    }

    switch (key) {
    case KEY_LEFT:
        if (mCursorLocation == 0) {
            return 0; // we're already at the beginning
        }
        if (mCursorLocation == -1) {
            if (displayValue.size() == 0) {
                cursorX = mRenderX;
                return 0;
            }
            mCursorLocation = displayValue.size() - 1;
        } else {
            mCursorLocation--;
        }
        mRendered = false;
        HandleCursorByText();
        return 0;

    case KEY_RIGHT:
        if (mCursorLocation == -1) {
            return 0; // we're already at the end
        }
        mCursorLocation++;
        if ((int) displayValue.size() <= mCursorLocation) {
            mCursorLocation = -1;
        }
        HandleCursorByText();
        mRendered = false;
        return 0;

    case KEY_HOME:
    case KEY_UP:
        if (displayValue.size() == 0) {
            return 0;
        }
        mCursorLocation = 0;
        mRendered = false;
        cursorX = mRenderX;
        return 0;

    case KEY_END:
    case KEY_DOWN:
        mCursorLocation = -1;
        mRendered = false;
        HandleCursorByText();
        return 0;
    }

    return 1;
}

int GUIInput::NotifyCharInput(int key)
{
    if (HasInputFocus) {
        if (key == KEYBOARD_BACKSPACE) {
            // Backspace
            if (mValue.size() > 0 && mCursorLocation != 0) {
                if (mCursorLocation == -1) {
                    mValue.resize(mValue.size() - 1);
                } else {
                    mValue.erase(mCursorLocation - 1, 1);
                    mCursorLocation--;
                }
                isLocalChange = true;
                DataManager::SetValue(mVariable, mValue);
                UpdateDisplayText();
                HandleTextLocation(TW_INPUT_NO_UPDATE);
                HandleCursorByText();
            }
        } else if (key == KEYBOARD_SWIPE_LEFT) {
            // Delete all
            isLocalChange = true;
            if (mCursorLocation == -1) {
                DataManager::SetValue (mVariable, "");
                mValue = "";
                textWidth = 0;
                mCursorLocation = -1;
            } else {
                mValue.erase(0, mCursorLocation);
                DataManager::SetValue(mVariable, mValue);
                mCursorLocation = 0;
            }
            UpdateDisplayText();
            cursorX = mRenderX;
            scrollingX = 0;
            mRendered = false;
            return 0;
        } else if (key >= 32) {
            // Regular key
            if (HasAllowed && AllowedList.find((char) key) == std::string::npos) {
                return 0;
            }
            if (HasDisabled && DisabledList.find((char) key) != std::string::npos) {
                return 0;
            }
            if (MaxLen != 0 && mValue.size() >= MaxLen) {
                return 0;
            }
            if (mCursorLocation == -1) {
                mValue += key;
            } else {
                mValue.insert(mCursorLocation, 1, key);
                mCursorLocation++;
            }
            isLocalChange = true;
            DataManager::SetValue(mVariable, mValue);
            UpdateDisplayText();
            HandleTextLocation(TW_INPUT_NO_UPDATE);
            HandleCursorByText();
        } else if (key == KEYBOARD_ACTION) {
            // Action
            if (mAction) {
                unsigned inputLen = mValue.length();
                if (inputLen < MinLen) {
                    return 0;
                } else if (MaxLen != 0 && inputLen > MaxLen) {
                    return 0;
                } else {
                    return (mAction ? mAction->NotifyTouch(TOUCH_RELEASE, mRenderX, mRenderY) : 1);
                }
            }
        }
        return 0;
    } else {
        if (key == 0) {
            // Somewhat ugly hack-ish way to tell the box to redraw after losing focus to remove the cursor
            mRendered = false;
            return 1;
        }
    }
    return 1;
}
